<?php

// ------------------------------------------------------------------------
// |@Author       : Jarmin <topextend@qq.com>
// |@----------------------------------------------------------------------
// |@Date         : 2025-05-07 17:03:41
// |@----------------------------------------------------------------------
// |@LastEditTime : 2025-05-11 14:58:05
// |@----------------------------------------------------------------------
// |@LastEditors  : Jarmin <topextend@qq.com>
// |@----------------------------------------------------------------------
// |@Description  :
// |@----------------------------------------------------------------------
// |@FilePath     : LoadRateLimit.php
// |@----------------------------------------------------------------------
// |@Copyright (c) 2025 http://www.ladmin.cn All Rights Reserved.
// ------------------------------------------------------------------------
declare (strict_types=1);

namespace think\admin\middleware;

use Closure;
use think\App;
use think\Request;
use think\Response;
use think\admin\traits\ResponseTrait;
use think\admin\traits\ErrorCodeTrait;

/**
 * 速率限制中间件
 *
 * 核心功能：
 * 1. 多策略请求限流（IP/用户/路径维度）
 * 2. 动态时间窗口管理（秒/分钟/小时/天）
 * 3. 黑白名单机制（IP/路径匹配）
 * 4. 标准化限流响应头（RFC 6585）
 *
 * @package think\admin\middleware
 * @version 1.2.0
 */
class LoadRateLimit
{
    use ResponseTrait;
    use ErrorCodeTrait;

    /**
     * @var App ThinkPHP应用实例
     *          通过依赖注入自动获取
     */
    protected $app;

    /**
     * @var array 限流配置（合并默认和用户配置）
     *
     * 配置结构：
     * - ratelimit.enable     : 全局开关
     * - ratelimit.default    : 默认策略
     *   - name   : 策略名称
     *   - key    : 限制维度（ip/user/path）
     *   - limit  : 时间窗口内最大请求数
     *   - window : 时间窗口（格式：数字+单位，如 10s/5m）
     * - ratelimit.strategies : 自定义策略数组
     *   - path   : 路径匹配模式（支持*通配符）
     *   - method : 请求方法匹配（可选）
     * - ratelimit.whitelist  : 白名单配置
     *   - ips    : IP白名单数组
     *   - paths  : 路径白名单模式
     * - ratelimit.blacklist  : 黑名单配置
     *   - ips    : IP黑名单数组
     *   - paths  : 路径黑名单模式
     */
    protected $config = [
        // 全局开
        'enable'     => true,
        // 默认策略
        'default'    => [
            'name'   => 'default',
            'key'    => 'ip',
            'limit'  => 60,
            'window' => '1m',
        ],
        // 自定义策略
        'strategies' => [],
        // 白名单配置
        'whitelist'  => [
            'ips'    => [],
            'paths'  => [],
        ],
        // 黑名单配置
        'blacklist'  => [
            'ips'    => [],
            'paths'  => [],
        ],
    ];

    /**
     * 构造函数（依赖注入）
     *
     * 功能说明：
     * 1. 通过依赖注入获取框架核心实例
     * 2. 合并用户自定义配置到默认配置
     *
     * 配置加载流程：
     * 1. 初始化中间件默认配置
     * 2. 加载用户配置文件（config/ratelimit.php）
     * 3. 深度合并配置参数
     *
     * 安全提示：
     * - 生产环境务必修改默认密钥
     * - 建议通过配置文件管理黑白名单
     *
     * @param App $app 框架核心实例（自动依赖注入）
     *                 包含缓存、配置等核心服务
     * @throws ConfigException 当用户配置格式错误时抛出
     */
    public function __construct(App $app)
    {
        $this->app = $app;
        // 合并配置
        $userConfig = config('ratelimit', []);
        $this->config = array_merge($this->config, $userConfig);
    }

    /**
     * 中间件处理入口
     *
     * 执行流程：
     * 1. 检查全局开关状态
     * 2. 匹配适用的限流策略
     * 3. 检查黑白名单
     * 4. 计算当前窗口请求计数
     * 5. 返回限流响应或继续处理
     *
     * @param Request $request 请求对象
     * @return Response 包含X-RateLimit头部的响应
     *
     * @example 限流响应示例：
     * HTTP/1.1 429 Too Many Requests
     * Retry-After: 60
     * X-RateLimit-Limit: 60
     * X-RateLimit-Remaining: 0
     * X-RateLimit-Reset: 1717048221
     */
    public function handle(Request $request, Closure $next): Response
    {
        if (isset($this->config['enable']) && $this->config['enable'] === false) {
            return $next($request);
        }

        $strategy = $this->getStrategy($request, $this->config);
        if (empty($strategy)) {
            return $next($request);
        }

        // 获取限流标识
        $key = $this->getLimitKey($request, $strategy);

        // 检查是否在白名单中
        if ($this->isInWhitelist($request, $this->config)) {
            return $next($request);
        }

        // 检查是否在黑名单中
        if ($this->isInBlacklist($request, $this->config)) {
            return $this->error('请求被拒绝', self::REQUEST_FORBIDDEN);
        }

        // 获取当前时间窗口的请求次数
        $count = (int)$this->app->cache->get($key, 0);
        $now = time();

        // 如果是新的时间窗口，重置计数器
        $windowExpire = $this->getWindowExpire($strategy['window']);
        if ($count === 0) {
            $this->app->cache->set($key, 1, $windowExpire);

            // 设置响应头
            $response = $next($request);
            return $this->setRateLimitHeaders($response, 1, $strategy['limit'], $windowExpire);
        }

        // 判断是否超出限制
        if ($count >= $strategy['limit']) {
            return $this->error('请求太频繁，请稍后再试', self::TOO_MANY_REQUESTS)
                ->header(['Retry-After' => $windowExpire]);
        }

        // 增加计数器
        $this->app->cache->inc($key);

        // 设置响应头
        $response = $next($request);
        return $this->setRateLimitHeaders($response, $count + 1, $strategy['limit'], $windowExpire);
    }

    /**
     * 获取限流策略
     *
     * 匹配优先级：
     * 1. 路径匹配策略（包含通配符）
     * 2. 请求方法匹配策略
     * 3. 默认策略
     *
     * @return array|null 匹配到的策略配置
     */
    protected function getStrategy(Request $request, array $config): ?array
    {
        $strategies = $config['strategies'] ?? [];
        $path = $request->pathinfo();

        foreach ($strategies as $strategy) {
            if (isset($strategy['path'])) {
                // 支持路径匹配
                if ($this->matchPath($path, $strategy['path'])) {
                    return $strategy;
                }
            } elseif (isset($strategy['method'])) {
                // 支持请求方法匹配
                if (in_array($request->method(), (array)$strategy['method'])) {
                    return $strategy;
                }
            }
        }

        // 返回默认策略
        return $config['default'] ?? null;
    }

    /**
     * 生成限流缓存键
     *
     * 键格式：
     * rate_limit:md5(标识符:策略名称)
     *
     * 标识符生成规则：
     * - ip   : 客户端IP
     * - user : 用户ID（未登录时回退IP）
     * - path : 请求路径
     */
    protected function getLimitKey(Request $request, array $strategy): string
    {
        $key = $strategy['key'] ?? 'ip';

        switch ($key) {
            case 'ip':
                $identifier = $request->ip();
                break;
            case 'user':
                $identifier = $request->uid ?? $request->ip();
                break;
            case 'path':
                $identifier = $request->pathinfo();
                break;
            default:
                $identifier = $request->ip();
        }

        return 'api_rate_limit:' . md5($identifier . ':' . ($strategy['name'] ?? 'default'));
    }

    /**
     * 检查是否在白名单中
     *
     * 白名单优先级：
     * 1. IP白名单（精确匹配）
     * 2. 路径白名单（支持通配符）
     *
     * @param Request $request 请求对象（自动注入）
     * @param array $config 限流配置
     * @return bool 是否在白名单中
     */
    protected function isInWhitelist(Request $request, array $config): bool
    {
        $whitelist = $config['whitelist'] ?? [];
        $ip = $request->ip();

        // IP白名单
        if (isset($whitelist['ips']) && in_array($ip, (array)$whitelist['ips'])) {
            return true;
        }

        // 路径白名单
        if (isset($whitelist['paths'])) {
            $path = $request->pathinfo();
            foreach ((array)$whitelist['paths'] as $pattern) {
                if ($this->matchPath($path, $pattern)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 检查是否在黑名单中
     *
     * 黑名单优先级：
     * 1. IP黑名单（精确匹配）
     * 2. 路径黑名单（支持通配符）
     *
     * @param Request $request 请求对象（自动注入）
     * @param array $config 限流配置
     * @return bool 是否在黑名单中
     */
    protected function isInBlacklist(Request $request, array $config): bool
    {
        $blacklist = $config['blacklist'] ?? [];
        $ip = $request->ip();

        // IP黑名单
        if (isset($blacklist['ips']) && in_array($ip, (array)$blacklist['ips'])) {
            return true;
        }

        // 路径黑名单
        if (isset($blacklist['paths'])) {
            $path = $request->pathinfo();
            foreach ((array)$blacklist['paths'] as $pattern) {
                if ($this->matchPath($path, $pattern)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 路径匹配（支持通配符）
     *
     * 匹配规则：
     * - * 匹配任意字符（除路径分隔符）
     * - ** 匹配多级目录（需特殊处理）
     *
     * @example /api/* 可匹配 /api/user 但不匹配 /api/v1/user
     * @example /admin/** 可匹配多级路径如 /admin/users/list
     */
    protected function matchPath(string $path, string $pattern): bool
    {
        $pattern = str_replace(['*', '/'], ['.*', '\/'], $pattern);
        return (bool)preg_match('/^' . $pattern . '$/', $path);
    }

    /**
     * 转换时间窗口为秒数
     *
     * 支持单位：
     * - s: 秒（默认）
     * - m: 分钟
     * - h: 小时
     * - d: 天
     *
     * @param string $window 时间窗口字符串（如：10s/5m）
     * @return int 窗口秒数（默认60秒）
     */
    protected function getWindowExpire(string $window): int
    {
        $unit = strtolower(substr($window, -1));
        $value = (int)substr($window, 0, -1);

        return match($unit) {
            's' => $value,
            'm' => $value * 60,
            'h' => $value * 3600,
            'd' => $value * 86400,
            default => 60,
        };
    }

    /**
     * 设置速率限制响应头
     *
     * 标准响应头：
     * - X-RateLimit-Limit: 窗口最大请求数
     * - X-RateLimit-Remaining: 剩余请求数
     * - X-RateLimit-Reset: 窗口重置时间戳
     */
    protected function setRateLimitHeaders(Response $response, int $current, int $limit, int $window): Response
    {
        return $response->header([
            'X-RateLimit-Limit' => $limit,
            'X-RateLimit-Remaining' => max(0, $limit - $current),
            'X-RateLimit-Reset' => time() + $window
        ]);
    }
}
